Add first-class APS OpenRTB integration#918
Conversation
aram356
left a comment
There was a problem hiding this comment.
Summary
Replaces the legacy APS TAM/contextual path with a first-class OpenRTB provider, adds a sandboxed opaque-origin creative renderer, a Prebid-adapter rendering route, PBS coexistence safeguards, and an inventory-identity override. The security core is well-hardened and I verified rather than assumed it — allow-same-origin is never combined with allow-scripts (pinned by a regression test), the nonce/envelope contract matches byte-for-byte across Rust and JS, and the two CodeQL alerts were genuinely resolved. CI is green.
That said, I found 7 blocking defects — an auction-wide failure from one malformed bid, two ways APS demand can still reach Prebid Server, two blank-slot / consumed-impression bugs in the new rendering routes, a privacy leak in the identity override, and a silent config break on upgrade. Requesting changes.
A process note: the PR body's change table documents only the first commit. Two later commits ("Add APS inventory identity overrides", "Render APS bids from the trustedServer Prebid adapter") added whole subsystems it omits — worth refreshing before merge.
Most findings are filed as inline comments. The cross-cutting ones are below.
Blocking
🔧 wrench
- APS exclusion is case-sensitive and leaks via the head-insert path (prebid.rs:1427/1434, prebid.rs:909): Both PBS guards compare
name == "aps"exactly, so"APS"/"Aps"in the free-formconfig.biddersslips through to PBS and is sold twice. Separately,head_insertsserializesconfig.bidders/client_side_biddersverbatim intowindow.__tsjs_prebid, soapsstill ships to the browser — the safeguard is server-side only, andclient_side_biddersis never filtered at all. The durable fix for this and the stored-request finding is to rejectapsin[prebid].bidders/client_side_biddersat config-validation time.
Non-blocking
🤔 thinking
- Full client-controlled Referer path+query now enters the bidstream (formats.rs:93-116):
site.pagechanged from a barehttps://{domain}to the full same-origin Referer including query string, so publisher URLs carrying PII (?email=, session tokens,gclid) are forwarded to every SSP — up to 8 KiB of attacker-chosen data (the host check constrains only the authority). This mirrors client-side Prebid so it may be intentional, but for a privacy-preserving edge server it deserves an explicit decision: strip query/fragment by default, or gate behind config. adserver_mockmediation is last-write-wins and the new test enshrines it (adserver_mock.rs:107, :899):build_bid_indexkeys on(provider, slot, bidder); if APS ever stops reducing to one bid per slot, the mediator restores the losing candidate's renderer against the winning bid's price — wrong creative at wrong price, silently.bid_idnow exists and would disambiguate; the mock only echoescrid. At least raise the collision log fromdebug!towarn!.providers.apsis now write-only and fails silently (creative_opportunities.rs:366,378): the field is a genuinedeny_unknown_fieldsdeser-compat shim (removing it hard-fails existing TOML), butto_ad_slotno longer reads it, so an operator who setsaps.slot_idexpecting routing gets a no-op with zero diagnostics. Add a load-time warning.
♻️ refactor
BidRenderer::aps()is an infallible accessor on a single-variant enum (types.rs:217-225, used at publisher.rs:1957): the moment a second variant lands the natural "fix" is a panic in the fallback arm. Preferas_aps(&self) -> Option<&ApsRendererV1>. Note the genericbid.bid_idfield this PR adds already carries the same value, so publisher.rs could derivehb_adidrenderer-agnostically.
📝 note
- Undocumented
/auctionwire-contract change: for renderer bids,cridgoes from always-present{bidder}-creativetobid.creative_id(absent when the bidder omitscrid),idfrom{bidder}-{slot}to the upstream bid id, andadmis omitted entirely. tsjs absorbs all three, butPOST /auctionis a documented OpenRTB endpoint; any non-tsjs consumer reading those fields breaks. The CHANGELOG's Breaking section doesn't mention the response shape. - Test quality across the new suites: the browser same-origin rejection test (aps-renderer.spec.ts:731-805) is effectively vacuous — a fixed
waitForTimeout(100)with no positive control, driven through a hand-rolled harness instead ofrenderApsCreative, so it would pass if the guard broke. Several sandbox assertions (:464,:555) assert the harness's own input rather than product behavior. And there is no JS-side boundary coverage for the size caps (envelope 256 KiB / base64 349528, account-id 1024, creative-url 4096) nor a regression test for the two blocking rendering-route bugs. (I did confirm the same-origin guard at aps.rs:74 is live, not dead code, via a browser test — so that check is real; the test just doesn't exercise it.)
⛏ nitpick
creative_idis the only renderer field without a length cap (aps.rs:682) — bounded by the 2 MiB body cap but inconsistent withaccount_id/creative_url.dropped_bid_countanddrop_reasonsdon't reconcile (aps.rs:776-799) — price-losers increment the count with no reason entry; the test assertsdropped=1, drop_reasons={}.- auction/README.md:120 ASCII box right border is off by one column.
trusted-server.example.tomlregressed a fictionalaps.example.com/e/dtb/bidto a real vendor host; several docs/tests use real*.amazon-adsystem.comendpoints against the project's "example.com only" rule. These are functionally-required vendor endpoints (not customer data or credentials) andamazon-adsystempredates this PR, so noting rather than blocking.
CI Status
- fmt: PASS
- clippy (all six adapter targets): PASS
- rust tests (fastly/axum/cloudflare/spin + parity): PASS
- js tests (vitest): PASS
- browser integration + CodeQL: PASS
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
First-class APS OpenRTB integration replacing the legacy contextual/TAM path, with a sandboxed (opaque-origin, nonce-gated, CSP-enforced) renderer that deliberately withholds adm from the client outside that sandbox. The core security design (bid parsing never reads adm, envelope byte-bounds, iframe sandbox without allow-same-origin, identity-checked postMessage, server-side CSP header) is solid and well-tested, including a genuinely adversarial browser test suite (nonce replay, CSP-only isolation with the sandbox attribute omitted, live same-origin attack attempts). The main blocker is that this branch does not currently merge cleanly with main — see Blocking #1, which is also the root cause of the two failing CI checks.
Blocking
🔧 wrench
- PR does not merge cleanly with
main:main's commit8bb5450dc(#890) added amake_test_bid_with_creative()helper inpublisher.rsthat builds aBid{}literal without this PR's newbid_id/creative_id/rendererfields. Rebasing/merging onto currentmainfails witherror[E0063]: missing fields— this is exactly what's failing thecargo fmtandcargo testCI checks shown on this PR (GitHub builds the synthetic merge commit, not the raw branch tip). See inline comment onauction/types.rs.
❓ question
- Debug mode can leak
admto the browser (crates/trusted-server-core/src/integrations/aps.rs:628): undermines the PR's core claim thatadmnever reaches the client outside the sandbox. See inline comment. - One bad bid can abort the entire
/auctionresponse (crates/trusted-server-core/src/auction/formats.rs:313): no multi-slot blast-radius test exists. See inline comment.
Non-blocking
♻️ refactor
Bid(types.rs:253) has no#[derive(Default)]despite every field type supporting it — deriving it directly prevents recurrence of Blocking #1 the next time a field is added.aps.rs:1169— no test exercises the fail-closed startup path (enabled+invalid config →register_providers()returningErr), onlyApsConfig::validate()directly.gpt/index.ts:999—renderingAdIdsgrows unbounded on the success path, inconsistent with sibling bounded caches added elsewhere in this PR.
🤔 thinking
aps.rs:1047— context-freeparse_responsesilently discards a valid response if ever called without context; safe today only because all real call sites useparse_response_with_context.aps.rs:380—device.languagehas no explicit byte cap, unlike other forwarded fields in this file.orchestrator.rs:28—backend_to_provider's bare 4-tuple is duplicated across 3 sites; same fragility class as Blocking #1.formats.rs:302— when bothbid.creativeandbid.rendererareSome,creativesilently wins with no log/assert.prebid.rs:1427— the"aps"bidder exclusion (preventing APS double-serving through Prebid Server) is case-sensitive; pre-existing pattern, wortheq_ignore_ascii_casegiven the stated goal.publisher.rs:2207— full client query string is now forwarded intosite.pagefor every provider; unlikeaps.rs(8192-byte cap),prebid.rs's equivalent has no bound.render.ts:325— rapid re-render of the same slot orphans the prior frame's message listener/timeout instead of routing through its own cleanup (self-heals within 10s).TESTING.md:49— still describes APS as "mocked" / lists "Implement real APS" as a future step, contradicting this PR and the log excerpt this PR itself just updated a few lines above.docs/guide/auction-orchestration.md:623— config reference table omitsinventory_domain/inventory_page_origin, real validated fields documented elsewhere in this PR.
⛏ nitpick
aps.rs:851— aseatbidentry with nobidarray is skipped without incrementingdrop_reasons, making a no-bid response harder to diagnose.render.test.ts:216— no test for "correct nonce, wrongevent.source" rejection specifically (the guard is implemented correctly atrender.ts:351, just not locked in by that exact case).render.ts:388— the embedded renderer-document string hand-duplicates constants (including the 10000ms timeout) instead of interpolating them.
🌱 seedling
- Byte-limit constants (
MAX_ACCOUNT_ID_BYTES, etc.) exist as 3 independently hardcoded copies across Rust, TS, and the embedded HTML string — worth a single source of truth eventually. creative_opportunities.rs:366— legacyproviders.aps.slot_idis now a silent no-op with no operator-facing warning if left configured.aps-renderer.spec.ts:169— no browser-level test for cross-ad-unit capability theft (themessageSourceBelongsToAdUnitguard exists and works, just isn't e2e-tested against a mismatched ad unit).
📝 note
auction/README.md:364— this example is stale relative to the new renderer flow (pre-existing drift, not touched by this PR).
👍 praise
aps.rs:parse_bidnever readsadm; envelope is byte-bounded and fixture-verified byte-for-byte between Rust and TS; CSP header (APS_RENDERER_CSP) is correctly sent server-side with noallow-same-origin.orchestrator.rs:provider_request_context/effective_timeoutthreading fix, pinned bydispatched_collection_reuses_provider_launch_context.render.ts:validateApsRenderer's exact-key allowlist cross-checking the outer descriptor vs. the decoded OpenRTB payload; theevent.source === iframe.contentWindowidentity check (the right check for an opaque-origin sandbox).prebid.rs: thetrustedServerbidder-expansion path correctly re-strips"aps"even when reintroduced viaconfig.bidders, with a dedicated regression test.aps-renderer.spec.ts: nonce-replay, CSP-only isolation (with the sandbox attribute omitted), and same-origin attack tests are genuinely adversarial — real attempts against a live server, not tautological checks of config strings.
CI Status
- fmt: FAIL (caused by the merge-conflict issue above, not this branch's own code)
- rust tests: FAIL (same root cause)
- clippy (all adapters): PASS
- cross-adapter parity: PASS
- browser/integration tests: PASS
- vitest: PASS
- CodeQL: PASS
7f448bc to
4847192
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Re-reviewed at 10cd6d3ce. Every finding from the previous review is fixed — I verified all seven blocking items and the non-blocking ones in source, and several were implemented exactly as suggested. CI is green across all 19 checks.
Two new blocking issues, both introduced by work landed since the last review: the fix for the case-sensitive APS exclusion over-corrected into a config hard-fail that can take a service down on upgrade, and the new debug-metadata commit captures upstream response headers without the fail-closed filtering this codebase applies elsewhere. Both have small fixes.
Blocking
🔧 wrench
- The
apsconfig rejection is an undocumented hard-fail that bricks every request (prebid.rs:346-368):config.validate()runs atsettings.rs:257before theis_enabled()check at line 265, andIntegrationRegistry::new(registry.rs:791) propagates the error with?. An operator whose published config containsbidders = ["aps", "kargo"]— previously harmless, silently stripped — deploys this WASM and every request fails, including when the prebid integration is disabled entirely. The CHANGELOG Breaking entry says operators must "disable native APS demand" but never states the config will refuse to boot;docs/guide/configuration.mddoesn't either. This PR's own tests construct["kargo", "aps"], so it is a config the codebase expects to exist in the wild. The hard-fail is also redundant with the runtime filter added in the same commit (lines 1571/1578 already stripapscase-insensitively). Either downgrade to warn-and-strip, or keep the error and add a dedicated**Breaking**CHANGELOG line plus a docs migration note — and confirmts config pushrejects it before the runtime does.
Non-blocking
🤔 thinking
- Skipped winners are still counted as wins in telemetry (formats.rs:341,351):
emit_auction_events_best_effort_lazy(... Completed { result })runs atauction/endpoints.rs:320, before response conversion, andtelemetry.rs:697-711emitsis_win = 1per winning bid. A bid dropped by the newcontinueis reported as a win in analytics but never delivered, andendpoints.rs:331still logs the pre-drop count. There's also no machine-readable drop counter — only alog::warn!— which is inconsistent with thedrop_reasonsmetadata this same PR adds on the APS side. Consider counting skips intoResponseExt.orchestrator, or emitting the terminal event after conversion. - An all-APS page now reports a Prebid provider error rather than a no-bid (prebid.rs:1598-1604): when every slot is APS-only the
impvector is empty and the existing guard returnsErr(TrustedServerError::Prebid), surfacing asBidStatus::Errorinprovider_details. That's a normal configuration, not a failure, and it will pollute provider error rates and alerting. Suggest an explicit no-bid response for "all imps intentionally dropped", reservingErrfor "no valid banner formats". - Debug body capture inherits the 2 MiB upstream cap (aps.rs:945-956, 970-978): captured bytes pass through
String::from_utf8_lossy(3 bytes per invalid byte) and then JSON escaping, so an adversarial 2 MiB upstream body can inflate the/auctionresponse to several MB, buffered in WASM heap. Per-auction rather than per-bid, so it doesn't accumulate, but a dedicated smaller preview cap mirroringMAX_BID_CREATIVE_DUMP_BYTESwould be safer. - The Referer privacy fix is only half-applied:
/auctionsanitizes, but the SSR path buildspublisher.page_urlatpublisher.rs:2018asscheme://host{request_path_and_query}— full query string and edge host, unfiltered — and passes it onward. Worth routing both producers through one sanitizer. Related:HeaderValue::from_strfailing on a non-ASCII URL silently dropsRefererwith no log. - Debug
requestbodyis reconstructed, not captured (aps.rs:596-605):debug_request()re-runsbuild_openrtb_requestat parse time and presents the result as what was sent. That holds only while the function stays pure. If it ever gains a uuid, timestamp, or any mutation between build and send, debug will silently report a payload APS never received and an operator will chase a fabricated request. Worth a test asserting the debug body byte-matches the captured outbound body, plus a doc comment recording the invariant. - One browser assertion passes vacuously (aps-renderer.spec.ts:364-366):
expect(result.foreignUniversalCreativeResponse).not.toEqual(expect.objectContaining({ apsRenderer }))succeeds when the promise times out toundefined, and would also succeed if the bridge served a slightly different renderer. The intended contract is "a foreign frame gets nothing" — asserttoBeUndefined().
♻️ refactor
validateApsRenderernow runs 3–4× per APS bid on the critical path: admission (prebid/index.ts:213), registration (render.ts:210), and the GPT bridge (gpt/index.ts:998). Each pass allocates aTextEncoder, base64-decodes up to 256 KB, andJSON.parses it. The admission call is new. Consider caching the validated descriptor on first success.- Four-element anonymous tuple in
backend_to_provider(orchestrator.rs:28, 460, 877-880):(String, Instant, Arc<dyn AuctionProvider>, u32)destructured as(provider_name, start_time, _, _)at five call sites, with an un-self-describingu32. A named struct would match the convention the project applies to argument lists.
📝 note
- Two trade-offs here came from my own suggestions last round — flagging so they get an explicit decision rather than sliding in as side effects. (1) Suffix host matching (formats.rs:110-119) is implemented correctly — the
prefix.ends_with('.')check properly rejectsevilpublisher.example— but it does mean any subdomain can now determine thesite.pageevery SSP sees; on a deployment with user-content subdomains that is worth a conscious accept. (2) Unconditionalset_query(None)is good for privacy but collapses every page of a query-driven site (/index.php?id=123) to a single URL, degrading contextual targeting and per-page reporting. Consider a key allowlist or a config switch, and document the trade-off either way. debugflag discoverability (aps.rs:132-136): the one-line doc comment carries no risk warning, whereas the analogous flags insettings.rs:1897-1923each have multi-line "never enable in production" rustdoc. An operator auditing the[debug]section also won't find this switch, since it lives under[integrations.aps]. Worth mirroring the stronger wording.
⛏ nitpick
attach_debug_metadatais skipped only on themissing_request_contextbranch (aps.rs:996-1002), contradicting the docs' claim that the exchange is emitted for success, 204, malformed and non-success statuses. Unreachable today, so latent rather than live.- The debug
requestheadersmap is a hardcodedcontent-type: application/json(aps.rs:604-608) rather than the headers actually sent at line 1042. They agree today and will drift; it also slightly misrepresents the PBShttpcallsshape it mirrors. - On failed renderer registration the descriptor is now retained on the bid (prebid/index.ts:546-552). I checked reachability and it is effectively defensive-only — admission already validated the descriptor,
adIdis Prebid-generated, andvalidPrebidIdentityaccepts GAM-style codes — so this is hygiene, not a live bug. publisher.rs:2139still usesexpect("should serialize typed renderer")whileformats.rs:331-342was hardened to handle the identical serialization failing; worth resolving the asymmetry in one direction.
CI Status
- fmt: PASS
- clippy (all six adapter targets): PASS
- rust tests (fastly/axum/cloudflare/spin + parity): PASS
- js tests (vitest): PASS
- browser integration + CodeQL: PASS
| ] { | ||
| if bidders | ||
| .iter() | ||
| .any(|bidder| bidder.eq_ignore_ascii_case("aps")) |
There was a problem hiding this comment.
🔧 wrench — This rejection is an undocumented hard-fail that takes down every request, not just auctions.
config.validate() runs at settings.rs:257 before the is_enabled() check at line 265, and IntegrationRegistry::new (registry.rs:791) propagates the error with ?. So an operator whose already-published config contains bidders = ["aps", "kargo"] — previously harmless, since to_openrtb silently stripped it — deploys this WASM and every request fails. That happens even if the prebid integration is disabled, because validation precedes the enabled check.
The CHANGELOG's Breaking entry says operators must "disable native APS demand for Trusted Server cohorts" but never states that an existing config will refuse to boot, and docs/guide/configuration.md doesn't either. This PR's own tests construct ["kargo", "aps"], so it's a config the codebase expects to exist in the wild.
It's also redundant with the runtime filter added in the same commit — lines 1571 and 1578 already strip aps case-insensitively, so demand cannot reach PBS regardless.
Either downgrade to warn-and-strip:
if bidders.iter().any(|b| b.eq_ignore_ascii_case("aps")) {
log::warn!("integrations.prebid.{field} includes APS; ignoring — configure APS under [integrations.aps]");
}or keep the hard error and add a dedicated **Breaking** CHANGELOG line plus a docs migration note, and confirm ts config push rejects it before the runtime does.
| }) | ||
| } | ||
|
|
||
| fn debug_headers(headers: &HeaderMap) -> BTreeMap<String, Vec<String>> { |
There was a problem hiding this comment.
🔧 wrench — Upstream response headers are captured wholesale, including set-cookie.
debug_headers copies every header into responseheaders, which lands in the client-visible /auction JSON via ext.orchestrator.provider_details[].metadata.debug. If APS ever returns set-cookie (or any auth / ID-sync header), its value is exposed to page JavaScript.
This contradicts the pattern this codebase already established for the same class of data — DEBUG_DUMP_METADATA_ALLOWLIST (publisher.rs:878) is deliberately fail-closed, with the comment that the "visitor's identity graph cannot reach the client-readable DOM even when debug is also enabled."
Gated behind default-off debug with a startup warning, so it isn't exploitable by default — but the fix is cheap and the precedent is clear:
const DEBUG_HEADER_DENYLIST: &[&str] = &["set-cookie", "authorization", "proxy-authorization"];
// ...
if DEBUG_HEADER_DENYLIST.contains(&name.as_str()) { continue; }| values | ||
| } | ||
|
|
||
| fn debug_request( |
There was a problem hiding this comment.
🤔 thinking — The debug requestbody is reconstructed, not captured.
debug_request() re-runs build_openrtb_request(request, context) at parse time and presents the result as the request that was sent. That's true only while build_openrtb_request stays a pure function of (request, context) — which this commit's orchestrator change (replaying the launch-time context) currently preserves.
The moment someone adds a uuid / timestamp / Instant-derived field, or mutates the body between build and send in request_bids, debug will report a payload APS never received — silently, with nothing failing. An operator debugging a no-bid would chase a fabricated request.
Worth pinning the invariant with a test (StubHttpClient already captures outbound bodies via request_bodies):
let sent = stub.request_bodies().pop().expect("should capture outbound APS body");
assert_eq!(
response.metadata["debug"]["httpcalls"]["aps"][0]["requestbody"],
String::from_utf8_lossy(&sent).as_ref(),
"debug requestbody should byte-match the request actually sent"
);Minor, same area: requestheaders (line 604) is a hardcoded content-type: application/json rather than the headers actually set at line 1042 — they agree today, will drift, and it slightly misrepresents the PBS httpcalls shape being mirrored.
| bid.bidder | ||
| ); | ||
| String::new() | ||
| continue; |
There was a problem hiding this comment.
🤔 thinking — Skipped winners are still reported as wins by telemetry.
This continue correctly stops a render-source-less bid from failing the whole response (the previous review's finding), but emit_auction_events_best_effort_lazy(... AuctionTerminalOutcome::Completed { result }) already ran at auction/endpoints.rs:320 — before conversion — and telemetry.rs:697-711 emits is_win = 1 per winning bid. So a bid dropped here is counted as a win in analytics but never delivered, and endpoints.rs:331 still logs the pre-drop count.
There's also no machine-readable drop counter — only the log::warn! — which is inconsistent with the drop_reasons metadata this same PR adds on the APS side.
Suggest counting skips and surfacing them in ResponseExt.orchestrator (e.g. dropped_winners), and/or moving the terminal event emission to after conversion.
| ); | ||
| return fallback; | ||
| } | ||
| parsed.set_query(None); |
There was a problem hiding this comment.
📝 note — Flagging two trade-offs that came from my own suggestions last round, so they get an explicit decision rather than sliding in as side effects.
Query stripping (this line): good for privacy — it keeps ?email=/session tokens out of the bidstream — but it collapses every page of a query-driven site (/index.php?id=123, ?p=456) to a single URL, which degrades contextual targeting, per-page reporting and frequency capping for those publishers. Fragment stripping is free; query stripping isn't. Consider a query-key allowlist or a config switch, and document the trade-off either way.
Suffix host matching (lines 110-119): implemented correctly — the prefix.ends_with('.') check properly rejects evilpublisher.example — but it does mean any subdomain can now determine the site.page that every SSP sees and that reporting keys on. On a deployment with user-content or delegated subdomains that deserves a conscious accept, and a line in the doc comment.
No action needed if both are intended; I'd just rather they be chosen than inherited.
| } | ||
| } | ||
|
|
||
| if excluded_aps && bidder.is_empty() { |
There was a problem hiding this comment.
🤔 thinking — An all-APS page now surfaces as a Prebid provider error rather than a no-bid.
When every slot is APS-only, the new return None drops all imps, imp ends up empty, and this guard returns Err(TrustedServerError::Prebid) — which shows up as BidStatus::Error in provider_details. That's a perfectly normal configuration, not a failure, so it will inflate provider error rates and trip alerting for operators mid-migration.
Suggest returning an explicit no-bid AuctionResponse for the "all imps intentionally dropped" case, and reserving Err for the genuine "no valid banner formats" condition.
| markRendered: () => markBidAsRendered(rawBid), | ||
| } | ||
| ); | ||
| if (registered) { |
There was a problem hiding this comment.
⛏ nitpick — Retaining the descriptor on failed registration inverts the previous invariant ("keep the capability only in the bounded registry"), so a rejected bid keeps an up-to-~350 KB base64 object that Prebid then hands to every analytics adapter and getBidResponses() consumer.
I checked reachability before flagging: admission at line 213 already runs validateApsRenderer, adId is Prebid-generated and matches /^[A-Za-z0-9-]+$/, validPrebidIdentity accepts GAM-style slash codes, and registry capacity evicts rather than failing. So this path is effectively defensive-only — hygiene rather than a live bug. Still, deleting the field unconditionally and warning separately would keep the invariant clean.
| expect(auctionRequests).toBe(1); | ||
| expect(result.acceptedAd).toBe(""); | ||
| expect(result.acceptedAdId).not.toBe(apsRenderer.bidId); | ||
| expect(result.foreignUniversalCreativeResponse).not.toEqual( |
There was a problem hiding this comment.
🤔 thinking — This assertion passes vacuously. expect(result.foreignUniversalCreativeResponse).not.toEqual(expect.objectContaining({ apsRenderer })) succeeds whenever the promise times out to undefined, and would also succeed if the bridge served a slightly different renderer to the foreign frame. The contract being tested is "a foreign frame gets nothing at all" — assert that directly:
expect(result.foreignUniversalCreativeResponse).toBeUndefined();(The rest of this spec is much stronger after the rework — the same-origin test now drives the real tsjs.requestAds() path with a genuine positive control, which is exactly right.)
Summary
/e/pb/bid.Changes
crates/trusted-server-core/src/integrations/aps.rscrates/trusted-server-core/src/auction/*adm.crates/trusted-server-core/src/integrations/prebid.rscrates/trusted-server-core/src/{publisher.rs,creative_opportunities.rs}crates/trusted-server-js/lib/src/integrations/aps/render.tscrates/trusted-server-js/lib/src/integrations/gpt/index.tscrates/trusted-server-integration-tests/browser/tests/shared/aps-renderer.spec.tstrusted-server.example.tomland integration fixturesaccount_id, the APS OpenRTB endpoint, and default-offallow_script_creatives.docs/**,CHANGELOG.md,TESTING.mdCloses
Closes #764
Test plan
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute servecargo test-cloudflare,cargo test-spin, all Cloudflare/Spin clippy aliases, 13 cross-adapter parity tests, browser TypeScript compilation, full Next.js/WordPress Playwright suite, TSJS bundle build, and VitePress buildChecklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)Rollout note
Broad production rollout and production
tagtype=scriptenablement remain gated on controlled APS-account validation and APS account-team confirmation. Script creatives remain disabled by default.